package fregat.connector;

import fregat.base.database.DatabaseConnector;
import fregat.base.net.ServiceLauncher;
import fregat.base.util.ConfigurationBundle;
import fregat.base.util.Logger;
import fregat.base.util.StopWatch;
import fregat.connector.channel.AbstractChannel;
import fregat.connector.worker.DefaultWorker;

import java.io.PrintStream;
import java.sql.CallableStatement;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.ResourceBundle;
import java.util.StringTokenizer;

/**
 * The <code>ConnectorService</code> is a service that handles communication
 * over multiple TCP/IP connections that may be configured in a property file.
 * For each connection a channel(<code>ServerChannel</code> or
 * <code>ClientChannel</code>) is created and transfers the received data to a
 * <code>TCPWorker</code> that processes the data. The worker may use the channel
 * to send data to the communication partner.<br>
 * This is the main class that starts all channels and workers
 *
 * @author Alexander Mann <br><b><i>ECO_L O G</i> &nbsp Logistiksysteme GmbH</b>
 * @version 1.0.2
 */
public class ConnectorService extends ServiceLauncher {
  private static ConnectorConfiguration configuration; // configuration of channels and workers
  private static DatabaseConnector databaseConnector; // database connection
  private static boolean fatalError; // flag for a fatal configuration error

  public static ResourceBundle resource
      = ResourceBundle.getBundle("at.ecolog.connector.resource"); // text resources
  public static String COMMUNICATION_LOGGER = "communication"; // logging category for telegrams

  public static final String VERSION = "ConnectorService.2.00.005";

  /**
   * Starts the <code>ConnectorService</code> service.<br>
   * Usage: ConnectorService <properties file>
   *
   * @param args Property file name
   */
  public static void main(String[] args) {
    if (args.length > 0) {

      if (args[0].equalsIgnoreCase("SHUTDOWN")) {
        // sends shutdown command to remote console
        ConfigurationBundle configuration = null;
        if (args.length > 1) {
          configuration = new ConfigurationBundle(args[1], false);
        }
        ServiceLauncher.shutdown(configuration, "connector", 4741);

      } else {
        //ServiceLauncher.setLockFile(args[0]);
        if (!ServiceLauncher.checkLock()) {
          new ConnectorService(args[0]).start();
        }
      }
    } else {
      System.out.println(resource.getString("ConnectorService_main_1"));
      System.out.println(resource.getString("ConnectorService_main_2"));
    }
  }

  /**
   * @return Version string
   */
  public String getName() {
    return VERSION;
  }

  /**
   * Defines available user commands by adding the definition strings to
   * {@link #commands}. The processing of the commands is implemented in
   * {@link #parseInput(java.lang.String,java.io.PrintStream)}.
   */
  protected void defineCommands() {
    commands.addElement(resource.getString("ConnectorService_defineCommands_1"));
    commands.addElement(resource.getString("ConnectorService_defineCommands_2"));
    commands.addElement(resource.getString("ConnectorService_defineCommands_3"));
    commands.addElement(resource.getString("ConnectorService_defineCommands_4"));
    commands.addElement(resource.getString("ConnectorService_defineCommands_5"));
    commands.addElement(resource.getString("ConnectorService_defineCommands_6"));
    super.defineCommands();
  }

  /**
   * Opens the specified configuration file and starts channels and workers.
   *
   * @param configurationFile Property file with connector configuration
   */
  public ConnectorService(String configurationFile) {
    LOGGING_CATEGORY = "connector";

    configuration = new ConnectorConfiguration(configurationFile);

    // configure channels(and workers) after configuration is created
    // as some workers might need the configuration instance
    configuration.configureChannels();
  }

  /**
   * Sends data on the specified channel.
   *
   * @param channelName Configuration name of channel
   * @param sendData    Data to send
   * @return True, if send was success
   */
  public static synchronized boolean send(String channelName, byte[] sendData) {
    AbstractChannel channel = configuration.getChannel(channelName);
    boolean success = false;
    if (channel != null) {
      String str = channelName + ", " + resource.getString("ConnectorService_send_1") + " '" + new String(sendData) + "'";
      debug(str);
      Logger.getCategory(COMMUNICATION_LOGGER).info(str);
      success = channel.send(sendData);
      
      if(!success) {
        warn(resource.getString("ConnectorService_send_3").replaceFirst("%1", channelName));
      }

    } else {
      warn(resource.getString("ConnectorService_send_2").replaceFirst("%1", channelName));
    }
    return success;
  }

  /**
   * Informs channel that all data was sent.
   *
   * @param channelName
   */
  public static synchronized void dataComplete(String channelName) {
    AbstractChannel channel = configuration.getChannel(channelName);
    if (channel != null) {
      channel.dataComplete();
    } else {
      warn(resource.getString("ConnectorService_dataComplete_1").replaceFirst("%1", channelName));
    }
  }

  /**
   * This method handles user commands.
   *
   * @param input User command
   * @param out   Output stream of connected console
   */
  protected void parseInput(String input, PrintStream out) {
    StringTokenizer tokenizer = new StringTokenizer(input, " ");
    String command = tokenizer.nextToken();

    if (command.equalsIgnoreCase("CHANNELS")) {
      // display all channels

      Iterator i = configuration.channels();
      while (i.hasNext()) {
        parseInput("STATE " + ((AbstractChannel) i.next()).getName(), out);
      }
      return;
    } else if (command.equalsIgnoreCase("WORKERS")) {
      // display all workers
      Iterator i = configuration.workers();
      while (i.hasNext()) {
        println(i.next().toString(), out);
      }
      return;
    }

    String name = tokenizer.nextToken();

    if (command.equalsIgnoreCase("CMD")) {
      // forward command to worker
      Object worker = configuration.getWorker(name);

      if (worker != null && worker instanceof DefaultWorker) {
        StringBuffer buffer = new StringBuffer();

        while (tokenizer.hasMoreTokens()) {
          buffer.append(tokenizer.nextToken());
          if (tokenizer.hasMoreTokens()) {
            buffer.append(" ");
          }
        }
        ((DefaultWorker) worker).receivedCommand(buffer.toString(), out);
      }

    } else {
      // these commands require a channel
      AbstractChannel channel = configuration.getChannel(name);
      if (channel == null) {
        println(resource.getString("ConnectorService_parseInput_1").replaceFirst("%1", name), out);
        return;
      }

      if (command.equalsIgnoreCase("SEND")) {
        // send any data
        String data = input.substring(command.length() + name.length() + 2, input.length());
        send(name, data.getBytes());

      } else if (command.equalsIgnoreCase("RECEIVE")) {
        String data = input.substring(command.length() + name.length() + 2, input.length());
        channel.received(data.getBytes());
        
      } else if (command.equalsIgnoreCase("STOP")) {
        // stop channel
        channel.shutdown();

      } else if (command.equalsIgnoreCase("START")) {
        if (configuration.containsChannel(name)) {
          channel.shutdown(); // make sure channel is closed
          channel = configuration.createChannel(name); // (re)create channel
          channel.start();
        }
      } else if (command.equalsIgnoreCase("STATE")) {
        println(channel.toString(), out);
      } else if (command.equalsIgnoreCase("COMPLETE")) {
        dataComplete(channel.getName());
      }
    }
  }

  /**
   * Starts the channels and workers.
   */
  public void start() {
    if (!fatalError) {
      String log = getName() + " " + resource.getString("ConnectorService_start_1");
      System.out.println(log);
      Logger.getCategory(LOGGING_CATEGORY).info(log);

      // start remote or local console
      super.start(configuration, "connector", 4741);
      
      String service_name = configuration.getConfigurationString("connector.name");
      if (service_name == null)
    	  service_name = "";

      // inform database that connector has started
      stateChanged(service_name, 1);

      // start the channels(workers are started by channel)
      Iterator i = configuration.channels();
      while (i.hasNext() && !shutdown) {
        ((AbstractChannel) i.next()).start();
      }

      // start the standalone workers(workers that are not connected to a channel)
      i = configuration.workers();
      boolean standalone = false;
      while (i.hasNext() && !shutdown) {
        Object worker = i.next();
        if (worker instanceof DefaultWorker && ((DefaultWorker) worker).isStandalone()) {
          ((DefaultWorker) worker).start();
          standalone = true;
        }
      }

      // when there are no active channels or standalone workers, the service may shutdown
      if (AbstractChannel.getInstances() == 0 && !standalone) {
        shutdown();
      }
    } else {
      error(resource.getString("ConnectorService_start_2"));
    }
  }

  /**
   * Stops all channels(and workers).
   */
  public void shutdown() {
    super.shutdown();
    
    String service_name = configuration.getConfigurationString("connector.name");
    if (service_name == null)
  	  service_name = "";

    // inform database that connector is shutting down
    stateChanged(service_name, 0);

    // wait configured timeout
    try {
      Thread.sleep(configuration.getConfigurationInteger("connector.shutdown.timeout"));
    } catch (Exception ex) {
    }

    // shutdown channels
    Iterator i = configuration.channels();
    while (i.hasNext()) {
      ((AbstractChannel) i.next()).shutdown();
    }

    // shutdown standalone workers
    i = configuration.workers();
    while (i.hasNext()) {
      Object worker = i.next();
      if (worker instanceof DefaultWorker && ((DefaultWorker) worker).isStandalone()) {
        ((DefaultWorker) worker).shutdown();
      }
    }

    // shutdown database connection
    if (databaseConnector != null) {
      databaseConnector.shutdown();
    }
  }

  /**
   * Informs the database about connection state changes by calling the
   * configured procedure.
   *
   * @param channelName Configuration name of channel (blank for service state)
   * @param state       0 .. shutdown/disconnected, 1 .. started/connected
   */
  public static synchronized void stateChanged(String channelName, int state) {
    String sql = configuration.getConfigurationString("connector.db.state");
    if (sql != null) {
      String log = "state -> " + DatabaseConnector.logStatement(sql + "('?',?)", new String[]{channelName, Integer.toString(state)});
      sql = "begin " + sql + "(?,?); end;";
      if (databaseConnector == null) {
        databaseConnector = configuration.configureDatabase("connector");
        databaseConnector.connectToDatabase();
        databaseConnector.setName(VERSION);
        databaseConnector.setAction("State");
      }

      try {
        CallableStatement statement = databaseConnector.getStatement("state");
        if (statement == null) {
          statement = databaseConnector.prepareStatement("state", sql);
        }
        statement.setString(1, channelName);
        statement.setInt(2, state);

        long executionTime = databaseConnector.executeStatement("state");

        if (StopWatch.isEnabled()) {
          note(log + " " + executionTime + "[ms]");
        }

        databaseConnector.commit();
      } catch (SQLException ex) {
        databaseConnector.rollback();
        ConnectorService.error("ConnectorService.stateChanged", ex);
      }
    }
  }

  /**
   * @return Configuration wrapper
   */
  public static ConnectorConfiguration getConfiguration() {
    return configuration;
  }

  /**
   * This method is to be called on a fatal configuration error.
   * The service will not be started then.
   */
  public static void fatalError() {
    fatalError = true;
  }

  /**
   * Informs the database about fault situations
   * This user exit can be used to generate e.g. so called
   * "Stoermeldungen"
   *
   * @param message Message to be sent
   */
  public static void faultMessage(String message) {

    String sql = configuration.getConfigurationString("connector.db.fault");
    if (sql != null) {
      String log = "fault -> " + DatabaseConnector.logStatement(sql + "('?')", new String[]{message});
      sql = "begin " + sql + "(?); end;";
      try {
        if (databaseConnector == null) {
          databaseConnector = configuration.configureDatabase("connector");
          databaseConnector.connectToDatabase();
          databaseConnector.setName(VERSION);
          databaseConnector.setAction("State");
        }

        CallableStatement statement = databaseConnector.getStatement("fault");
        if (statement == null) {
          statement = databaseConnector.prepareStatement("fault", sql);
        }
        statement.setString(1, message);

        long executionTime = databaseConnector.executeStatement("fault");

        if (StopWatch.isEnabled()) {
          note(log + " " + executionTime + "[ms]");
        }

        databaseConnector.commit();
      } catch (SQLException ex) {
        databaseConnector.rollback();
        ConnectorService.error("ConnectorService.faultMessage", ex);
      }
    }


  }

}
